Repository navigation
Close the plugin's task-queue vocabulary drift, then gate it (vikunja#558, #543) - #10
Merged
Merged
Conversation
added 3 commits
August 29, 2026 08:20
…#558, #543) The plugin carried four partial hand-written copies of a vocabulary it does not own -- task-queue-mcp's src/tools/queue.py. None of them learned about `routing-failed` when the MCP made it a first-class non-terminal status, so the status most in need of an operator sorted below `cancelled`, rendered the same grey as `parked`, and was not offered by the status filter. `manual-then-auto` was the same omission one field over. Fixed - routing-failed sorts above in-progress, renders in `error`, is filterable, and is a valid destination in the status-change control. Setting it by hand is not a dead end: the dispatcher's routing-failed pass picks up any such task whose retry window has passed, and a record with no next_retry_at is eligible immediately -- so it reads as "re-route now, skipping re-approval". - The detail view gains a banner saying what routing-failed means, how many retries are used, when the next attempt is due, and that the end of the budget is a dead letter. - A queued `manual-then-auto` now survives the Start button. toWorkflowMode collapsed it to `semi-auto`, re-pinning every task the session spawns back to `semi-auto` -- the exact failure vikunja#533 added the mode to fix. Added - src/vocabulary.ts: one copy of the vocabulary plus the UI maps keyed BY it. STATUS_ORDER and STATUS_COLOR are Record<Status, ...>, so adding a status without a sort position and a colour is a tsc error rather than a silent fallthrough to `?? 9` and `muted`. That is the half the old code lacked -- Record<string, ...> accepts anything and covers nothing. - npm run gate:vocabulary, as its own CI step. Ported from task-dispatcher's test_task_queue_vocabulary.py (vikunja#324) keeping both properties that make it work: no skip-on-no-network path, and it tracks `main` not a pin. Proven to fire before shipping -- exit 0 in sync, 1 on real drift against an older upstream ref, 2 with no network, 2 on a traversal-shaped ref. - A Mode filter and a mode badge. A task with no recorded workflow_mode renders as unknown, not as the queue's default. Changed - The two NON_TERMINAL_STATUSES literals are one derived constant. - Relative imports use .ts, not .js: node --test runs the sources directly and does not rewrite a .js specifier to the .ts beside it, so the panels were not importable by a test. 110 tests pass; each new assertion verified red against the defect it covers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> agent-id: developer
Caught by the pre-audit baseline. The routing-failed banner interpolated `new Date(next_retry_at).toLocaleString()` directly, so a malformed value in the queue YAML rendered the literal string "Invalid Date" at the operator. It reaches textContent, not innerHTML, so this was never an injection -- it is the same failure as the launch-log birthtime case: a date the code could not read, presented as though it could. Now the banner names it as unreadable and keeps the rest of the message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> agent-id: developer
agent-workflow-interop-2026-08-phase2 audit: 0 findings, 2 Info, both no action. INFO 1 was the deliberate absence of `redirect: "manual"` on the vocabulary gate's fetch, self-disclosed before the audit ran and confirmed acceptable. Marked SECURITY[accepted] where the decision lives, with the row in host-forge-knowledge-base/security/accepted-risks.md. Also adds the in-build "Invalid Date" fix (91acf66) to the 0.8.0 changelog entry, which it was missing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> agent-id: developer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Build
agent-workflow-interop-2026-08Phase 2. Closes vikunja#558, folding in #543 — same four files, same defect class.The drift
task-queue-mcpmaderouting-faileda first-class non-terminal status and counts it inactiveonGET /queue/summary. This plugin carried four partial hand-written copies of a vocabulary it does not own, and none of them were updated:panels/task-list.tsSTATUS_ORDER?? 9— a retrying, failing task sorted belowcancelled, dead lastpanels/task-list.tsNON_TERMINAL_STATUSESpanels/task-detail.tsDETAIL_NON_TERMINAL_STATUSESpanels/styles.tsstatusColorcase, sodefault: muted— identical toparkedandcancelledThe status most in need of an operator was the one that sorted last and looked least urgent. Nothing detected it: a missing key in a
Record<string, …>is not a type error, and aswitchwith adefaulthas no missing case.What changed
routing-failedis a known status everywhere. Sorts abovein-progress— first in its group — renderserrorrather thanmuted, is offered by the status filter, and is a valid destination in the operator status-change control. Setting it by hand is not a dead end: the dispatcher'sprocess_routing_failedpass picks up any such task whose retry window has passed, andis_eligiblereturns true whennext_retry_atis absent — so it reads as "re-route now, skipping re-approval", which is a thing an operator wants.A
routing-failedtask explains itself. The detail view gains a banner naming what the dispatcher is doing, retries used, next attempt, and that the end of the budget is a dead letter.A queued
manual-then-autosurvives the Start button (#543).toWorkflowModecollapsed it tosemi-auto, which re-pins every task the session spawns back tosemi-auto— the exact failure vikunja#533 added the mode to fix (four security→steward return tasks sat unactioned for over a week).reviewon such a task now passesmanual-then-autothrough;run-steward.shalready accepts it and does the downgrade. An explicitautoStart still overrides, and the launch note says which it did. This was a live behavioural bug, not just a missing badge.src/vocabulary.tsnow holds one copy of the vocabulary plus the UI maps keyed by it.STATUS_ORDERandSTATUS_COLORareRecord<Status, …>, so adding a status without a sort position and a colour is atscerror. That is the half the old code lacked —Record<string, number>accepts anything and covers nothing.Also: a Mode filter and a mode badge (rows badge
autoandmanual-then-auto, stay quiet aboutsemi-auto, which is 98% of the queue); a task with no recordedworkflow_moderenders as unknown rather than as the queue's default, because older records predate the field and inventing a value the queue never wrote is how this class of bug starts.The gate
npm run gate:vocabularyfetchestask-queue-mcp'smainand asserts the sets match. Ported fromtask-dispatcher/tests/test_task_queue_vocabulary.py(vikunja#324) keeping both properties that make that one work:main, not a pin. A vocabulary change merged upstream turns this repo red on the next push. That is the alarm, not a malfunction.TASK_QUEUE_MCP_REFexists only for a paired pre-merge change from a shell; CI does not set it, and the ref is charset-constrained so it cannot traverse to another repo's path onraw.githubusercontent.com.It is its own CI step, after
npm test. A red vocabulary means "go editsrc/vocabulary.ts" — a different instruction from any unit failure — and folded into one command either could hide the other.Nothing fetched is executed and no Python runs: a narrow tokenizer (
src/gates/python-sets.ts, split out so it is testable) accepts a brace-delimited list of plain string literals and refuses everything else, including the two upstream constructs that must not parse as sets — the derivedNON_TERMINAL_STATUSES = VALID_STATUSES - TERMINAL_STATUSESand the annotatedVALID_TRANSITIONS: dict[...].Proof it fires
A gate nobody has seen fail is not yet a gate. All four outcomes verified before this was opened:
mainTASK_QUEUE_MCP_REF=v0.8.2(a real upstream ref predatingmanual-then-auto)VALID_WORKFLOW_MODES — plugin-only: [manual-then-auto]TASK_QUEUE_MCP_REF=../../evil/mainThe new assertions were each verified red against the defect they cover, by mutating the fix back:
routing-failedsorting last (2 fail), rendering muted (1 fail), omitted fromNON_TERMINAL_STATUSES(2 fail),toWorkflowModeflatteningmanual-then-auto(2 fail), the parser accepting a derived set (1 fail), and the parser matching an annotated assignment (1 fail). Baseline green in every case after restore.Also
Relative imports now use
.ts, not.js.node --testruns the sources directly under type stripping and does not rewrite a.jsspecifier to the.tsfile beside it — the panels all used.jsand were simply never imported by a test; the first one that needed to be, was not.Verification
npm run build(tsc + esbuild) cleannpm test— 110 pass, 0 fail (was 85)npm run gate:vocabulary— in syncThe UI half (sort position, colour, banner, filter, badge) needs eyes in CloudCLI after
./deploy.sh && pm2 restart cloudcli—pm2is sysadmin's, so that goes out as a deploy task.🤖 Generated with Claude Code